package com.grapier.gwt.client.ui.dragdrop;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.EventPreview;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.Widget;

/**
 * Wraps a Widget and makes it drag-aware.  You can add any number of DragListeners
 * to this widget.  When a drag action happens on the widget, we will call the
 * startDragging, drag, and endDragging methods of all DragListeners in the obvious
 * way.  Note that you need to call setWidget() on this class to set an 
 * actual widget you wish to drag!
 * 
 * @author chungwu
 */
public class DragAwareWidget extends SimplePanel implements EventPreview {
  
  /**
   * DragListener is an object that listens for drag events generated by dragging
   * on this DragAwareWidget.
   */
  public interface DragListener {
    /**
     * Drag has started at (mouseX, mouseY)
     */
    public void startDragging(int mouseX, int mouseY);
    
    /**
     * Drag has moved to (mouseX, mouseY), which is (deltaX, deltaY) from
     * the original starting drag position
     */
    public void drag(int mouseX, int mouseY, int deltaX, int deltaY);
    
    /**
     * Drag has ended at (mouseX, mouseY), which is (deltaX, deltaY) from
     * the original starting drag position
     */
    public void endDragging(int mouseX, int mouseY, int deltaX, int deltaY);
  }
  
  private boolean dragging = false;
  private int dragStartX;
  private int dragStartY;
  private List listeners = null;
  
  public DragAwareWidget(Widget widget) {
    super();
    this.setWidget(widget);
    this.listeners = new ArrayList();
    
    // we want to listen to mouse events on this widget
    DOM.sinkEvents(getElement(), Event.MOUSEEVENTS);
  }
  
  /**
   * Adds a DragListener to this widget.  DragListeners are called for
   * each drag event in the order that they are added.
   */
  public void addDragListener(DragListener listener) {
    listeners.add(listener);
  }

  /**
   * Start the drag on mouse down
   */
  private void dragStart(int mouseX, int mouseY) {
    // make sure this widget captures all consequent mouse events
    DOM.setCapture(getElement());
    
    // record the starting mouse positions
    dragStartX = mouseX;
    dragStartY = mouseY;

    // add this as an event preview to handle mousemove and mouseup
    // events.  This makes drag-drop a lot snappier than going through
    // the usual mouselistener channel, since an event preview gets
    // an event before anything else, and can prevent events from
    // uselessly propagating
    DOM.addEventPreview(this);
    
    dragging = true;
    
    // fire off the startDragging event
    for (Iterator iter = listeners.iterator(); iter.hasNext(); ) {
      ((DragListener) iter.next()).startDragging(mouseX, mouseY);
    }
  }
  
  /**
   * Move the drag on mouse move
   */
  private void dragMove(int mouseX, int mouseY) {
    if (dragging) {
      // calculate the deltas
      int deltaX = (mouseX - dragStartX);
      int deltaY = (mouseY - dragStartY);

      // fire the drag event
      for (Iterator iter = listeners.iterator(); iter.hasNext(); ) {
        ((DragListener) iter.next()).drag(mouseX, mouseY, deltaX, deltaY);
      }
    }
  }

  /**
   * End the drag on mouse up
   */
  private void dragEnd(int mouseX, int mouseY) {
    if (dragging) {
      // remove this as an event preview
      DOM.removeEventPreview(this);
      dragging = false;
    
      // release capture of mouse events
      DOM.releaseCapture(getElement());
      
      // fire the endDragging event
      int deltaX = (mouseX - dragStartX);
      int deltaY = (mouseY - dragStartY);
      for (Iterator iter = listeners.iterator(); iter.hasNext(); ) {
        ((DragListener) iter.next()).endDragging(mouseX, mouseY, deltaX, deltaY);
      }
    }
  }

  /**
   * Listen for the ONMOUSEDOWN event and starts a drag
   */
  public void onBrowserEvent(Event event) {
    switch (DOM.eventGetType(event)) {
      case Event.ONMOUSEDOWN:
        int x = DOM.eventGetClientX(event);
        int y = DOM.eventGetClientY(event);
        DOM.eventPreventDefault(event);
        dragStart(x, y);
        break;
    }
  }

  /**
   * Listen for the ONMOUSEMOVE and ONMOUSEUP events and either drag
   * or end the drag
   */
  public boolean onEventPreview(Event event) {
    int x = DOM.eventGetClientX(event);
    int y = DOM.eventGetClientY(event);
    switch (DOM.eventGetType(event)) {
    case Event.ONMOUSEUP:
      dragEnd(x, y);
      break;
    case Event.ONMOUSEMOVE:
      dragMove(x, y);
      break;
    }

    // Do not allow the event to fire for anything else
    return true;
  }
}